/*---------------------------------------------------------------------------------------------
 *  Copyright (c) Microsoft Corporation. All rights reserved.
 *  Licensed under the MIT License. See License.txt in the project root for license information.
 *--------------------------------------------------------------------------------------------*/

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Text.RegularExpressions;
using UnityEngine;
using SimpleJSON;
using IOPath = System.IO.Path;

namespace Microsoft.Unity.VisualStudio.Editor
{
	/// <summary>
	/// 基于 VS Code 的编辑器安装类，支持 Windsurf、Cursor、traeCN、Lingma 等编辑器
	/// </summary>
	internal class VSCodeBasedEditorInstallation : VisualStudioInstallation
	{
		private static readonly IGenerator _generator = GeneratorFactory.GetInstance(GeneratorStyle.SDK);

		/// <summary>
		/// 编辑器类型枚举
		/// </summary>
		internal enum EditorType
		{
			Windsurf,
			Cursor,
			TraeCN,
			Lingma,
			Qoder,
			Kiro,
		}

		internal EditorType Type { get; set; }

		public override bool SupportsAnalyzers
		{
			get
			{
				return true;
			}
		}

		public override Version LatestLanguageVersionSupported
		{
			get
			{
				return new Version(13, 0);
			}
		}

		private string GetExtensionPath()
		{
			var editorConfigDir = GetEditorConfigDirectory();
			if (string.IsNullOrEmpty(editorConfigDir))
				return null;

			var extensionsPath = IOPath.Combine(editorConfigDir, "extensions");
			if (!Directory.Exists(extensionsPath))
				return null;

			return Directory
				.EnumerateDirectories(extensionsPath, $"{MicrosoftUnityExtensionId}*")
				.OrderByDescending(n => n)
				.FirstOrDefault();
		}

		private string GetEditorConfigDirectory()
		{
			var userProfile = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);

			switch (Type)
			{
				case EditorType.Windsurf:
					return IOPath.Combine(userProfile, ".windsurf");
				case EditorType.Cursor:
					return IOPath.Combine(userProfile, ".cursor");
				case EditorType.TraeCN:
					return IOPath.Combine(userProfile, ".traecn");
				case EditorType.Lingma:
					return IOPath.Combine(userProfile, ".lingma");
				case EditorType.Qoder:
					return IOPath.Combine(userProfile, ".qoder");
				default:
					return null;
			}
		}

		public override string[] GetAnalyzers()
		{
			var extensionPath = GetExtensionPath();
			if (string.IsNullOrEmpty(extensionPath))
				return Array.Empty<string>();

			return GetAnalyzers(extensionPath);
		}

		public override IGenerator ProjectGenerator
		{
			get
			{
				return _generator;
			}
		}

		private static bool IsCandidateForDiscovery(string path, EditorType editorType)
		{
			if (string.IsNullOrEmpty(path))
				return false;

			var patterns = GetEditorPatterns(editorType);

#if UNITY_EDITOR_OSX
			return Directory.Exists(path) && patterns.Any(p => Regex.IsMatch(path, p, RegexOptions.IgnoreCase));
#elif UNITY_EDITOR_WIN
			return File.Exists(path) && patterns.Any(p => Regex.IsMatch(path, p, RegexOptions.IgnoreCase));
#else
			return File.Exists(path) && patterns.Any(p => path.EndsWith(p, StringComparison.OrdinalIgnoreCase));
#endif
		}

		private static string[] GetEditorPatterns(EditorType editorType)
		{
			switch (editorType)
			{
				case EditorType.Windsurf:
#if UNITY_EDITOR_OSX
					return new[] { @".*Windsurf.*\.app$" };
#elif UNITY_EDITOR_WIN
					return new[] { @".*Windsurf.*\.exe$" };
#else
					return new[] { "windsurf", "windsurf-code" };
#endif
				case EditorType.Cursor:
#if UNITY_EDITOR_OSX
					return new[] { @".*Cursor.*\.app$" };
#elif UNITY_EDITOR_WIN
					return new[] { @".*Cursor.*\.exe$" };
#else
					return new[] { "cursor", "cursor-code" };
#endif
				case EditorType.TraeCN:
#if UNITY_EDITOR_OSX
					return new[] { @".*TraeCN.*\.app$", @".*trae.*\.app$" };
#elif UNITY_EDITOR_WIN
					return new[] { @".*TraeCN.*\.exe$", @".*trae.*\.exe$" };
#else
					return new[] { "traecn", "trae" };
#endif
				case EditorType.Lingma:
#if UNITY_EDITOR_OSX
					return new[] { @".*Lingma.*\.app$", @".*lingma.*\.app$" };
#elif UNITY_EDITOR_WIN
					return new[] { @".*Lingma.*\.exe$", @".*lingma.*\.exe$" };
#else
					return new[] { "lingma", "lingma-code" };
#endif
				case EditorType.Qoder:
#if UNITY_EDITOR_OSX
					return new[] { @".*Qoder.*\.app$" };
#elif UNITY_EDITOR_WIN
					return new[] { @".*Qoder.*\.exe$" };
#else
					return new[] { "qoder", "qoder-code" };
#endif
				case EditorType.Kiro:
#if UNITY_EDITOR_OSX
					return new[] { @".*Kiro.*\.app$" };
#elif UNITY_EDITOR_WIN
					return new[] { @".*Kiro.*\.exe$" };
#else
					return new[] { "kiro", "kiro-code" };
#endif
				default:
					return Array.Empty<string>();
			}
		}

		private static string GetEditorDisplayName(EditorType editorType)
		{
			switch (editorType)
			{
				case EditorType.Windsurf:
					return "Windsurf";
				case EditorType.Cursor:
					return "Cursor";
				case EditorType.TraeCN:
					return "TraeCN";
				case EditorType.Lingma:
					return "Lingma";
				case EditorType.Qoder:
					return "Qoder";
				case EditorType.Kiro:
					return "Kiro";
				default:
					return "Unknown Editor";
			}
		}

		[Serializable]
		internal class EditorManifest
		{
			public string name;
			public string version;
		}

		public static bool TryDiscoverInstallation(string editorPath, EditorType editorType, out IVisualStudioInstallation installation)
		{
			installation = null;

			if (string.IsNullOrEmpty(editorPath))
				return false;

			if (!IsCandidateForDiscovery(editorPath, editorType))
				return false;

			Version version = null;

			try
			{
				var manifestBase = GetRealPath(editorPath);

#if UNITY_EDITOR_WIN
				manifestBase = IOPath.GetDirectoryName(manifestBase);
#elif UNITY_EDITOR_OSX
				manifestBase = IOPath.Combine(manifestBase, "Contents");
#else
				var parent = Directory.GetParent(manifestBase);
				manifestBase = parent?.Name == "bin" ? parent.Parent?.FullName : parent?.FullName;
#endif

				if (manifestBase == null)
					return false;

				var manifestFullPath = IOPath.Combine(manifestBase, "resources", "app", "package.json");
				if (File.Exists(manifestFullPath))
				{
					var manifest = JsonUtility.FromJson<EditorManifest>(File.ReadAllText(manifestFullPath));
					Version.TryParse(manifest.version.Split('-').First(), out version);
				}
			}
			catch (Exception)
			{
				// 如果无法获取版本号，不失败
			}

			installation = new VSCodeBasedEditorInstallation()
			{
				Type = editorType,
				Name = GetEditorDisplayName(editorType) + (version != null ? $" [{version.ToString(3)}]" : string.Empty),
				Path = editorPath,
				Version = version ?? new Version()
			};

			return true;
		}

		public static IEnumerable<IVisualStudioInstallation> GetVisualStudioInstallations(EditorType editorType)
		{
			var candidates = new List<string>();

#if UNITY_EDITOR_WIN
			var localAppPath = IOPath.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "Programs");
			var programFiles = IOPath.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles));
			var appData = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData);

			foreach (var basePath in new[] { localAppPath, programFiles })
			{
				switch (editorType)
				{
					case EditorType.Windsurf:
						candidates.Add(IOPath.Combine(basePath, "Windsurf", "Windsurf.exe"));
						candidates.Add(IOPath.Combine(appData, "Windsurf", "Windsurf.exe"));
						break;
					case EditorType.Cursor:
						candidates.Add(IOPath.Combine(basePath, "Cursor", "Cursor.exe"));
						candidates.Add(IOPath.Combine(appData, "Cursor", "Cursor.exe"));
						break;
					case EditorType.TraeCN:
						candidates.Add(IOPath.Combine(basePath, "TraeCN", "TraeCN.exe"));
						candidates.Add(IOPath.Combine(appData, "TraeCN", "TraeCN.exe"));
						break;
					case EditorType.Lingma:
						candidates.Add(IOPath.Combine(basePath, "Lingma", "Lingma.exe"));
						candidates.Add(IOPath.Combine(appData, "Lingma", "Lingma.exe"));
						break;
					case EditorType.Qoder:
						candidates.Add(IOPath.Combine(basePath, "Qoder", "Qoder.exe"));
						candidates.Add(IOPath.Combine(appData, "Qoder", "Qoder.exe"));
						break;
				}
			}
#elif UNITY_EDITOR_OSX
			var appPath = IOPath.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles));
			var homeAppPath = IOPath.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), "Applications");

			switch (editorType)
			{
				case EditorType.Windsurf:
					candidates.AddRange(Directory.EnumerateDirectories(appPath, "Windsurf*.app").Concat(
						Directory.EnumerateDirectories(homeAppPath, "Windsurf*.app")));
					break;
				case EditorType.Cursor:
					candidates.AddRange(Directory.EnumerateDirectories(appPath, "Cursor*.app").Concat(
						Directory.EnumerateDirectories(homeAppPath, "Cursor*.app")));
					break;
				case EditorType.TraeCN:
					candidates.AddRange(Directory.EnumerateDirectories(appPath, "TraeCN*.app").Concat(
						Directory.EnumerateDirectories(homeAppPath, "TraeCN*.app")));
					candidates.AddRange(Directory.EnumerateDirectories(appPath, "Trae*.app").Concat(
						Directory.EnumerateDirectories(homeAppPath, "Trae*.app")));
					break;
				case EditorType.Lingma:
					candidates.AddRange(Directory.EnumerateDirectories(appPath, "Lingma*.app").Concat(
						Directory.EnumerateDirectories(homeAppPath, "Lingma*.app")));
					break;
				case EditorType.Qoder:
					candidates.AddRange(Directory.EnumerateDirectories(appPath, "Qoder*.app").Concat(
						Directory.EnumerateDirectories(homeAppPath, "Qoder*.app")));
					break;
			}
#elif UNITY_EDITOR_LINUX
			// Linux 常见位置
			var basePaths = new List<string>
			{
				"/usr/bin",
				"/usr/local/bin",
				IOPath.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".local", "bin")
			};

			foreach (var basePath in basePaths)
			{
				switch (editorType)
				{
					case EditorType.Windsurf:
						candidates.Add(IOPath.Combine(basePath, "windsurf"));
						candidates.Add(IOPath.Combine(basePath, "windsurf-code"));
						break;
					case EditorType.Cursor:
						candidates.Add(IOPath.Combine(basePath, "cursor"));
						candidates.Add(IOPath.Combine(basePath, "cursor-code"));
						break;
					case EditorType.TraeCN:
						candidates.Add(IOPath.Combine(basePath, "traecn"));
						candidates.Add(IOPath.Combine(basePath, "trae"));
						break;
					case EditorType.Lingma:
						candidates.Add(IOPath.Combine(basePath, "lingma"));
						candidates.Add(IOPath.Combine(basePath, "lingma-code"));
						break;
					case EditorType.Qoder:
						candidates.Add(IOPath.Combine(basePath, "qoder"));
						candidates.Add(IOPath.Combine(basePath, "qoder-code"));
						break;
				}
			}
			candidates.AddRange(GetXdgCandidates(editorType));
#endif

			foreach (var candidate in candidates.Distinct())
			{
#if UNITY_EDITOR_OSX
				if (Directory.Exists(candidate))
#else
				if (File.Exists(candidate))
#endif
				{
					if (TryDiscoverInstallation(candidate, editorType, out var installation))
						yield return installation;
				}
			}
		}

#if UNITY_EDITOR_LINUX
		private static readonly Regex DesktopFileExecEntry = new Regex(@"Exec=(\S+)", RegexOptions.Singleline | RegexOptions.Compiled);

		private static IEnumerable<string> GetXdgCandidates(EditorType editorType)
		{
			var envdirs = Environment.GetEnvironmentVariable("XDG_DATA_DIRS");
			if (string.IsNullOrEmpty(envdirs))
				yield break;

			var desktopFileNames = GetDesktopFileNames(editorType);
			var dirs = envdirs.Split(':');

			foreach (var dir in dirs)
			{
				foreach (var desktopFileName in desktopFileNames)
				{
					Match match = null;

					try
					{
						var desktopFile = IOPath.Combine(dir, $"applications/{desktopFileName}");
						if (!File.Exists(desktopFile))
							continue;

						var content = File.ReadAllText(desktopFile);
						match = DesktopFileExecEntry.Match(content);
					}
					catch
					{
						// 如果无法读取 desktop 文件，不失败
					}

					if (match != null && match.Success)
					{
						yield return match.Groups[1].Value;
					}
				}
			}
		}

		private static string[] GetDesktopFileNames(EditorType editorType)
		{
			switch (editorType)
			{
				case EditorType.Windsurf:
					return new[] { "windsurf.desktop", "windsurf-code.desktop" };
				case EditorType.Cursor:
					return new[] { "cursor.desktop", "cursor-code.desktop" };
				case EditorType.TraeCN:
					return new[] { "traecn.desktop", "trae.desktop" };
				case EditorType.Lingma:
					return new[] { "lingma.desktop", "lingma-code.desktop" };
				case EditorType.Qoder:
					return new[] { "qoder.desktop", "qoder-code.desktop" };
				default:
					return Array.Empty<string>();
			}
		}

		[System.Runtime.InteropServices.DllImport("libc")]
		private static extern int readlink(string path, byte[] buffer, int buflen);

		internal static string GetRealPath(string path)
		{
			byte[] buf = new byte[512];
			int ret = readlink(path, buf, buf.Length);
			if (ret == -1) return path;
			char[] cbuf = new char[512];
			int chars = System.Text.Encoding.Default.GetChars(buf, 0, ret, cbuf, 0);
			return new String(cbuf, 0, chars);
		}
#else
		internal static string GetRealPath(string path)
		{
			return path;
		}
#endif

		public override void CreateExtraFiles(string projectDirectory)
		{
			try
			{
				// 在项目目录下创建对应的配置目录（使用 .vscode 作为标准配置目录名，兼容 VS Code）
				// 这些基于 VS Code 的编辑器通常也支持 .vscode 配置目录
				var configDirectory = IOPath.Combine(projectDirectory.NormalizePathSeparators(), ".vscode");
				Directory.CreateDirectory(configDirectory);

				var enablePatch = !File.Exists(IOPath.Combine(configDirectory, ".vstupatchdisable"));

				CreateRecommendedExtensionsFile(configDirectory, enablePatch);
				CreateSettingsFile(configDirectory, enablePatch);
				CreateLaunchFile(configDirectory, enablePatch);
			}
			catch (IOException)
			{
			}
		}

		private const string DefaultLaunchFileContent = @"{
    ""version"": ""0.2.0"",
    ""configurations"": [
        {
            ""name"": ""Attach to Unity"",
            ""type"": ""vstuc"",
            ""request"": ""attach""
        }
     ]
}";

		private static void CreateLaunchFile(string configDirectory, bool enablePatch)
		{
			var launchFile = IOPath.Combine(configDirectory, "launch.json");
			if (File.Exists(launchFile))
			{
				if (enablePatch)
					PatchLaunchFile(launchFile);

				return;
			}

			File.WriteAllText(launchFile, DefaultLaunchFileContent);
		}

		private static void PatchLaunchFile(string launchFile)
		{
			try
			{
				const string configurationsKey = "configurations";
				const string typeKey = "type";

				var content = File.ReadAllText(launchFile);
				var launch = JSONNode.Parse(content);

				var configurations = launch[configurationsKey] as JSONArray;
				if (configurations == null)
				{
					configurations = new JSONArray();
					launch.Add(configurationsKey, configurations);
				}

				if (configurations.Linq.Any(entry => entry.Value[typeKey].Value == "vstuc"))
					return;

				var defaultContent = JSONNode.Parse(DefaultLaunchFileContent);
				configurations.Add(defaultContent[configurationsKey][0]);

				WriteAllTextFromJObject(launchFile, launch);
			}
			catch (Exception)
			{
				// 如果无法修补 launch.json 文件，不失败
			}
		}

		private void CreateSettingsFile(string configDirectory, bool enablePatch)
		{
			var settingsFile = IOPath.Combine(configDirectory, "settings.json");
			if (File.Exists(settingsFile))
			{
				if (enablePatch)
					PatchSettingsFile(settingsFile);

				return;
			}

			const string excludes = @"    ""files.exclude"": {
        ""**/.DS_Store"": true,
        ""**/.git"": true,
        ""**/.vs"": true,
        ""**/.gitmodules"": true,
        ""**/.vsconfig"": true,
        ""**/*.booproj"": true,
        ""**/*.pidb"": true,
        ""**/*.suo"": true,
        ""**/*.user"": true,
        ""**/*.userprefs"": true,
        ""**/*.unityproj"": true,
        ""**/*.dll"": true,
        ""**/*.exe"": true,
        ""**/*.pdf"": true,
        ""**/*.mid"": true,
        ""**/*.midi"": true,
        ""**/*.wav"": true,
        ""**/*.gif"": true,
        ""**/*.ico"": true,
        ""**/*.jpg"": true,
        ""**/*.jpeg"": true,
        ""**/*.png"": true,
        ""**/*.psd"": true,
        ""**/*.tga"": true,
        ""**/*.tif"": true,
        ""**/*.tiff"": true,
        ""**/*.3ds"": true,
        ""**/*.3DS"": true,
        ""**/*.fbx"": true,
        ""**/*.FBX"": true,
        ""**/*.lxo"": true,
        ""**/*.LXO"": true,
        ""**/*.ma"": true,
        ""**/*.MA"": true,
        ""**/*.obj"": true,
        ""**/*.OBJ"": true,
        ""**/*.asset"": true,
        ""**/*.cubemap"": true,
        ""**/*.flare"": true,
        ""**/*.mat"": true,
        ""**/*.meta"": true,
        ""**/*.prefab"": true,
        ""**/*.unity"": true,
        ""build/"": true,
        ""Build/"": true,
        ""Library/"": true,
        ""library/"": true,
        ""obj/"": true,
        ""Obj/"": true,
        ""Logs/"": true,
        ""logs/"": true,
        ""ProjectSettings/"": true,
        ""UserSettings/"": true,
        ""temp/"": true,
        ""Temp/"": true
    }";

			var content = @"{
" + excludes + @",
    ""files.associations"": {
        ""*.asset"": ""yaml"",
        ""*.meta"": ""yaml"",
        ""*.prefab"": ""yaml"",
        ""*.unity"": ""yaml"",
    },
    ""explorer.fileNesting.enabled"": true,
    ""explorer.fileNesting.patterns"": {
        ""*.sln"": ""*.csproj"",
        ""*.slnx"": ""*.csproj""
    },
    ""dotnet.defaultSolution"": """ + IOPath.GetFileName(ProjectGenerator.SolutionFile()) + @"""
}";

			File.WriteAllText(settingsFile, content);
		}

		private void PatchSettingsFile(string settingsFile)
		{
			try
			{
				const string excludesKey = "files.exclude";
				const string solutionKey = "dotnet.defaultSolution";

				var content = File.ReadAllText(settingsFile);
				var settings = JSONNode.Parse(content);

				var excludes = settings[excludesKey] as JSONObject;
				if (excludes == null)
					return;

				var patchList = new List<string>();
				var patched = false;

				foreach (var exclude in excludes)
				{
					if (!bool.TryParse(exclude.Value, out var exc) || !exc)
						continue;

					var key = exclude.Key;

					if (!key.EndsWith(".sln") && !key.EndsWith(".csproj"))
						continue;

					if (!Regex.IsMatch(key, @"^(\*\*[\\\/])?\*\.(sln|csproj)$"))
						continue;

					patchList.Add(key);
					patched = true;
				}

				var defaultSolution = settings[solutionKey];
				var solutionFile = IOPath.GetFileName(ProjectGenerator.SolutionFile());
				if (defaultSolution == null || defaultSolution.Value != solutionFile)
				{
					settings[solutionKey] = solutionFile;
					patched = true;
				}

				if (!patched)
					return;

				foreach (var patch in patchList)
					excludes.Remove(patch);

				WriteAllTextFromJObject(settingsFile, settings);
			}
			catch (Exception)
			{
				// 如果无法修补 settings.json 文件，不失败
			}
		}

		private const string MicrosoftUnityExtensionId = "visualstudiotoolsforunity.vstuc";
		private const string DefaultRecommendedExtensionsContent = @"{
    ""recommendations"": [
      """ + MicrosoftUnityExtensionId + @"""
    ]
}
";

		private static void CreateRecommendedExtensionsFile(string configDirectory, bool enablePatch)
		{
			var extensionFile = IOPath.Combine(configDirectory, "extensions.json");
			if (File.Exists(extensionFile))
			{
				if (enablePatch)
					PatchRecommendedExtensionsFile(extensionFile);

				return;
			}

			File.WriteAllText(extensionFile, DefaultRecommendedExtensionsContent);
		}

		private static void PatchRecommendedExtensionsFile(string extensionFile)
		{
			try
			{
				const string recommendationsKey = "recommendations";

				var content = File.ReadAllText(extensionFile);
				var extensions = JSONNode.Parse(content);

				var recommendations = extensions[recommendationsKey] as JSONArray;
				if (recommendations == null)
				{
					recommendations = new JSONArray();
					extensions.Add(recommendationsKey, recommendations);
				}

				if (recommendations.Linq.Any(entry => entry.Value.Value == MicrosoftUnityExtensionId))
					return;

				recommendations.Add(MicrosoftUnityExtensionId);
				WriteAllTextFromJObject(extensionFile, extensions);
			}
			catch (Exception)
			{
				// 如果无法修补 extensions.json 文件，不失败
			}
		}

		private static void WriteAllTextFromJObject(string file, JSONNode node)
		{
			using (var fs = File.Open(file, FileMode.Create))
			using (var sw = new StreamWriter(fs))
			{
				sw.Write(node.ToString(aIndent: 4));
			}
		}

		public override bool Open(string path, int line, int column, string solution)
		{
			var application = Path;

			line = Math.Max(1, line);
			column = Math.Max(0, column);

			var directory = IOPath.GetDirectoryName(solution);
			var workspace = TryFindWorkspace(directory);

			var target = workspace ?? directory;

			ProcessRunner.Start(string.IsNullOrEmpty(path)
				? ProcessStartInfoFor(application, $"\"{target}\"")
				: ProcessStartInfoFor(application, $"\"{target}\" -g \"{path}\":{line}:{column}"));

			return true;
		}

		private static string TryFindWorkspace(string directory)
		{
			var files = Directory.GetFiles(directory, "*.code-workspace", SearchOption.TopDirectoryOnly);
			if (files.Length == 0 || files.Length > 1)
				return null;

			return files[0];
		}

		private static ProcessStartInfo ProcessStartInfoFor(string application, string arguments)
		{
#if UNITY_EDITOR_OSX
			arguments = $"-n \"{application}\" --args {arguments}";
			application = "open";
			return ProcessRunner.ProcessStartInfoFor(application, arguments, redirect: false, shell: true);
#else
			return ProcessRunner.ProcessStartInfoFor(application, arguments, redirect: false);
#endif
		}

		public static void Initialize()
		{
		}
	}
}
